/*
 * Copyright (c) 2004-2013 Tada AB and other contributors, as listed below.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the The BSD 3-Clause License
 * which accompanies this distribution, and is available at
 * http://opensource.org/licenses/BSD-3-Clause
 *
 * Contributors:
 *   Tada AB
 *   Filip Hrbek
 */
package org.postgresql.pljava.example;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Provides a {@link #callMetaDataMethod callMetaData} function taking a string
 * that supplies the name of, and arguments to, any {@code ResultSet}-returning
 * {@link DatabaseMetaData} method, and returns a single-column {@code varchar}
 * result, the first row a header, then one for each {@code ResultSet} row,
 * semicolons delimiting the original columns.
 * @author Filip Hrbek
 */
public class MetaDataTest {
	public static Iterator<String> callMetaDataMethod(String methodCall)
			throws SQLException {
		return new MetaDataTest(methodCall).iterator();
	}

	private String m_methodName;
	private Object[] m_methodArgs;
	private Class<?>[] m_methodArgTypes;
	private ArrayList<String> m_results;

	public MetaDataTest(String methodCall) throws SQLException {
		Connection conn = DriverManager
				.getConnection("jdbc:default:connection");
		DatabaseMetaData md = conn.getMetaData();
		ResultSet rs;
		m_results = new ArrayList<String>();
		StringBuffer result;

		parseMethodCall(methodCall);

		try {
			Method m = DatabaseMetaData.class.getMethod(m_methodName,
					m_methodArgTypes);
			if (!m.getReturnType().equals(ResultSet.class)) {
				throw new NoSuchMethodException("Unexpected return type");
			}

			rs = (ResultSet) m.invoke(md, m_methodArgs);
			ResultSetMetaData rsmd = rs.getMetaData();

			int cnt = rsmd.getColumnCount();
			result = new StringBuffer();
			for (int i = 1; i <= cnt; i++) {
				result.append((rsmd.getColumnName(i) + "("
						+ rsmd.getColumnClassName(i) + ")").replaceAll(
						"(\\\\|;)", "\\$1") + ";");
			}
			m_results.add(result.toString());

			while (rs.next()) {
				result = new StringBuffer();
				Object rsObject = null;
				for (int i = 1; i <= cnt; i++) {
					rsObject = rs.getObject(i);
					if (rsObject == null) {
						rsObject = "<NULL>";
					}

					result.append(rsObject.toString().replaceAll("(\\\\|;)",
							"\\$1")
							+ ";");
				}
				m_results.add(result.toString());
			}
			rs.close();
		} catch (NoSuchMethodException nme) {
			StringBuffer sb = new StringBuffer();
			for (int i = 0; i < m_methodArgTypes.length; i++) {
				if (sb.length() > 0) {
					sb.append(",");
				}
				sb.append(m_methodArgTypes[i].getName());
			}
			throw new SQLException(
					"No such method or non-resultset return type: "
							+ m_methodName + "(" + sb.toString() + ")");
		} catch (InvocationTargetException ite) {
			throw new SQLException(ite.getTargetException().toString());
		} catch (Exception e) {
			throw new SQLException("Method error: " + e.toString());
		}
	}

	public void close() {
	}

	private Iterator<String> iterator() {
		return m_results.iterator();
	}

	/**
	 * Method call parser. Let's say that only String, String[], int, int[] and
	 * boolean types are accepted. Examples: "foo" => String {"foo","bar"} =>
	 * String[] {} => String[] (zero length array, not null!) 10 => int -7 =>
	 * int [10,3] => int[] [] => int[] (zero length array, not null!) TRUE =>
	 * boolean (String)null => String (value null) (String[])null => String[]
	 * (value null) (int[])null => int[] (value null)
	 * 
	 * Complete example: select * from
	 * callMetaDataMethod('getTables((String)null,"sqlj","jar%",{"TABLE"})');
	 * 
	 * It makes the implementation simplier, and it is sufficient for
	 * DatabaseMetaData testing.
	 */
	private void parseMethodCall(String methodCall) throws SQLException {
		try {
			Pattern p = Pattern
					.compile("^\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\((.*)\\)\\s*$");
			Matcher m = p.matcher(methodCall);
			String paramString;
			String auxParamString;
			String param;
			ArrayList<Object> objects = new ArrayList<Object>();
			ArrayList<Class<?>> types = new ArrayList<Class<?>>();

			if (m.matches()) {
				m_methodName = m.group(1);
				paramString = m.group(2).trim();
				p = Pattern
						.compile("^\\s*("
								+ "\\(\\s*(?:String|int)\\s*(?:\\[\\s*\\])?\\s*\\)\\s*null|"
								+ // String, String[] or int[] null
								"TRUE|"
								+ // boolean TRUE
								"FALSE|"
								+ // boolean FALSE
								"(?:\\-|\\+)?[0-9]+|"
								+ // int
								"\\[((?:[^\\[\\]])*)\\]|"
								+ // int[]
								"\"((?:[^\\\\\"]|\\\\.)*)\"|"
								+ // String
								"\\{((?:[^\\{\\}]|\"(?:[^\\\\\"]|\\\\.)*\")*)\\}"
								+ // String[]
								")\\s*" + "(?:,|$)" + // comma separator
								"(.*)$"); // rest of the string
				auxParamString = paramString;
				while (!auxParamString.equals("")) {
					m = p.matcher(auxParamString);
					if (!m.matches()) {
						throw new SQLException("Invalid parameter list: "
								+ paramString);
					}

					param = m.group(1);
					if (param.startsWith("\"")) // it is a string
					{
						param = m.group(3); // string without the quotes
						objects.add(param);
						types.add(String.class);
					} else if (param.startsWith("{")) // it is a string array
					{
						param = m.group(4); // string array without the curly
											// brackets
						Pattern parr = Pattern
								.compile("^\\s*\"((?:[^\\\\\"]|\\\\.)*)\"\\s*(?:,|$)(.*)$");
						Matcher marr;
						String auxParamArr = param.trim();
						ArrayList<String> strList = new ArrayList<String>();

						while (!auxParamArr.equals("")) {
							marr = parr.matcher(auxParamArr);
							if (!marr.matches()) {
								throw new SQLException("Invalid string array: "
										+ param);
							}

							strList.add(marr.group(1));
							auxParamArr = marr.group(2).trim();
						}
						objects.add(strList.toArray(new String[0]));
						types.add(String[].class);
					} else if (param.equals("TRUE") || param.equals("FALSE")) // it
																				// is
																				// a
																				// boolean
					{
						objects.add(new Boolean(param));
						types.add(Boolean.TYPE);
					} else if (param.startsWith("(")) // it is String, String[]
														// or int[] null
					{
						Pattern pnull = Pattern
								.compile("^\\(\\s*(String|int)\\s*(\\[\\s*\\])?\\s*\\)\\s*null\\s*$");
						Matcher mnull = pnull.matcher(param);

						if (mnull.matches()) {
							objects.add(null);
							if (mnull.group(2) == null) {
								if (mnull.group(1).equals("String")) {
									types.add(String.class);
								} else {
									throw new SQLException(
											"Primitive 'int' cannot be null");
								}
							} else {
								if (mnull.group(1).equals("String")) {
									types.add(String[].class);
								} else {
									types.add(int[].class);
								}
							}
						} else {
							throw new SQLException("Invalid null value: "
									+ param);
						}
					} else if (param.startsWith("[")) // it is a int array
					{
						param = m.group(2); // int array without the square
											// brackets
						Pattern parr = Pattern
								.compile("^\\s*(\\d+)\\s*(?:,|$)(.*)$");
						Matcher marr;
						String auxParamArr = param.trim();
						ArrayList<Integer> intList = new ArrayList<Integer>();

						while (!auxParamArr.equals("")) {
							marr = parr.matcher(auxParamArr);
							if (!marr.matches()) {
								throw new SQLException("Invalid int array: "
										+ param);
							}

							intList.add(new Integer(marr.group(1)));
							auxParamArr = marr.group(2).trim();
						}
						objects.add(intList.toArray(new Integer[0]));
						types.add(int[].class);
					} else // it is an int
					{
						objects.add(new Integer(param));
						types.add(Integer.TYPE);
					}

					auxParamString = m.group(5).trim();
				}
			} else {
				throw new SQLException("Syntax error");
			}

			m_methodArgs = objects.toArray(new Object[0]);
			m_methodArgTypes = types.toArray(new Class[0]);
		} catch (Exception e) {
			throw new SQLException("Invalid method call: " + methodCall
					+ ". Cause: " + e.toString());
		}
	}
}
